package clasificador;

import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;

import datos.Atributo;
import datos.Dato;
import datos.MuestraClasificada;
import java.lang.reflect.InvocationTargetException;

public class ValidadorCruzado<Clase>
        implements Validador<Clase, MuestraClasificada<Clase>, ValidadorCruzado.ResultadosValidacionCruzada> {

    private Class<? extends ClasificadorEntrenado<Clase>> clasificadores;
    private int nDivisionesMuestra, nDivisionesTest;

    public ValidadorCruzado(Class<? extends ClasificadorEntrenado<Clase>> clasificadores,
            int nDivisionesMuestra, int nDivisionesTest) {
        this.clasificadores = clasificadores;
        this.nDivisionesMuestra = nDivisionesMuestra;
        this.nDivisionesTest = nDivisionesTest;
    }

    @Override
    public ResultadosValidacionCruzada validar(MuestraClasificada<Clase> muestra, Map<String, Object> parametrosClasificador) {
        List<MuestraClasificada<Clase>.RangoClasificado> rangos = getDatosTestValidacionCruzada(muestra);
        List<Double> errores = new ArrayList<>();

        for (MuestraClasificada<Clase>.RangoClasificado rango : rangos) {
            ClasificadorEntrenado<Clase> clasificador;
            Atributo<Clase> clase = muestra.getClase();
            MuestraClasificada<Clase> datosEntrenamiento = rango.getOutside();
            MuestraClasificada<Clase> datosTest = rango.getInside();

            /* Es aqui donde se produce el entrenamiento */
            /* Aunque no lo parezca */
            try {
                clasificador = clasificadores.getConstructor(new Class<?>[]{MuestraClasificada.class, Map.class})
                        .newInstance(datosEntrenamiento, parametrosClasificador);

            } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                throw new RuntimeException("Reflection exception: " + e.getMessage(), e);
            }

            Map<Dato, Clase> resultadosTest = clasificarDatosTest(clasificador, datosTest);
            errores.add(calcularError(resultadosTest, clase));
        }

        return new ResultadosValidacionCruzada(errores);
    }

    private Double calcularError(Map<Dato, Clase> resultadosTest, Atributo<Clase> clase) {
        int error = 0;
        int total = 0;

        for (Map.Entry<Dato, Clase> entry : resultadosTest.entrySet()) {
            if (!entry.getKey().getAtributo(clase).equals(entry.getValue())) {
                error++;
            }
            total++;
        }

        return new Double(error) / new Double(total);
    }

    private Map<Dato, Clase> clasificarDatosTest(ClasificadorEntrenado<Clase> clasificador,
            MuestraClasificada<Clase> muestraTest) {
        Map<Dato, Clase> resultados = new HashMap<>(muestraTest.size());

        for (Dato datoTest : muestraTest.getDatos()) {
            try {
                resultados.put(datoTest, clasificador.clasificar(datoTest));
            } catch (Throwable e) {
                throw new RuntimeException("Ocurrio un error al clasificar los datos de test. ", e);                
            }
        }

        return resultados;
    }

    private MuestraClasificada<Clase>.RangoClasificado getDatosTest(MuestraClasificada<Clase> muestra, int iTest) {
        int nDatos = muestra.getDatos().size();

        double sizeDivision = new Double(nDatos) / new Double(nDivisionesMuestra);

        int jTest = new Double(sizeDivision * iTest).intValue();

        /* Con toda la cara, si se sale del conjunto, trunco el conjunto de test */
        /* Lo suyo ser�a dar la vuelta y hacer un rango, por ejemplo, de 90 a 100 y de 0 a 10; pero nuestra definici�n de
         * RangoMuestra no permite eso, y he preferido no tocarla.
         * Como en la mayor�a de los casos, nDivisionesTest va a ser 1, da igual.
         */
        int jFinTest = Math.min(nDatos, jTest + new Double(sizeDivision * nDivisionesTest).intValue());

        return muestra.new RangoClasificado(jTest, jFinTest);
    }

    /* Devuelve varias particiones de validaci�n, para hacer validaci�n cruzada */
    private List<MuestraClasificada<Clase>.RangoClasificado> getDatosTestValidacionCruzada(MuestraClasificada<Clase> muestra) {
        List<MuestraClasificada<Clase>.RangoClasificado> result =
                new ArrayList<>(nDivisionesMuestra);

        for (int i = 0; i < nDivisionesMuestra; i++) {
            result.add(getDatosTest(muestra, i));
        }

        return result;
    }

    public static class ResultadosValidacionCruzada {

        private List<Double> errores;

        public ResultadosValidacionCruzada(List<Double> errores) {
            this.errores = errores;
        }

        public List<Double> getErrores() {
            return errores;
        }
    }
}
